﻿using System;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using xn;

namespace KinectAPI
{
    public class KinectAPIContext
    {
        #region Constants

        /// <summary>
        /// Default configuration file path.
        /// </summary>
        protected const string XML_CONFIGURATION = @"SamplesConfig.xml";

        /// <summary>
        /// Specified configuration file path:the .oni video played.
        /// </summary>
        protected const string ONI_CONFIGURATION = @"Sample.oni";

        /// <summary>
        /// Specified configuration file path:the .oni video recorded.
        /// </summary>
        protected const string RECORD_CONFIGURATION = @"Record.oni";

        /// <summary>
        /// Horizontal bitmap dpi.
        /// </summary>
        readonly int DPI_X = 96;

        /// <summary>
        /// Vertical bitmap dpi.
        /// </summary>
        readonly int DPI_Y = 96;

        #endregion

        #region Fields

        /// <summary>
        /// OpenNI context used for most OpenNI-related operations
        /// </summary>
        protected Context _context;

        /// <summary>
        /// Normal image
        /// </summary>
        protected ImageMetaData _imageMD;

        /// <summary>
        /// Depth image
        /// </summary>
        protected DepthMetaData _depthMD;

        /// <summary>
        /// Normal image generator
        /// </summary>
        protected ImageGenerator _imageGenerator;

        /// <summary>
        /// General user OpenNI creator 
        /// </summary>
        protected UserGenerator _userGenerator;

        /// <summary>
        /// BitMap image where the normal image will be copied
        /// </summary>
        protected WriteableBitmap _cameraImage;

        /// <summary>
        /// BitMap image where the depth image will be copied
        /// </summary>
        protected WriteableBitmap _cameraDepth;

        /// <summary>
        /// Depth image generator
        /// </summary>
        protected DepthGenerator _depthGenerator;

        protected SceneAnalyzer _scene;

        /// <summary>
        /// General thread 
        /// </summary>
        protected Thread _cameraThread;

        /// <summary>
        /// Recorder in a ONI file
        /// </summary>
        protected Recorder recorder;

        #endregion

        #region Properties

        /// <summary>
        /// Image width.
        /// </summary>
        public int Width { get; protected set; }

        /// <summary>
        /// Image height.
        /// </summary>
        public int Height { get; protected set; }

        /// <summary>
        /// OpenNI histogram.
        /// </summary>
        public int[] Histogram { get; private set; }

        /// <summary>
        /// Camera raw image source.
        /// </summary>
        public ImageSource RawImage
        {
            get
            {
                _cameraImage.Lock();
                unsafe
                {
                    //ushort* pImage = (ushort*)_imageGenerator.GetImageMapPtr().ToPointer();
                    //// Pour avoir que les pixels de user 0
                    //ushort* pLabels = (ushort*)_userGenerator.GetUserPixels(0).SceneMapPtr.ToPointer();
                    //for (int y = 0; y < _imageMD.YRes; ++y)
                    //{
                    //    byte* pDest = (byte*)_cameraImage.BackBuffer.ToPointer() + y * _cameraImage.BackBufferStride;
                    //    for (int x = 0; x < _imageMD.XRes; ++x, ++pLabels, pDest += 3)
                    //    {
                    //        if (x%2 == 0) pImage+=3;
                    //        // Initialise le pixel a 0
                    //        pDest[0] = pDest[1] = pDest[2] = 0;
                    //        ushort label = *pLabels;
                    //        bool shouldDrawBackground = true;
                    //        // si on doit dessiner le fond ou si c'est le user.
                    //        if (shouldDrawBackground || *pLabels != 0)
                    //        {
                    //            pDest[0] = (byte)pImage[0];
                    //            pDest[1] = (byte)pImage[1];
                    //            pDest[2] = (byte)pImage[2];
                    //        }
                    //    }
                    //}
                    //_cameraImage.AddDirtyRect(new Int32Rect(0, 0, _imageMD.XRes, _imageMD.YRes));

                    _cameraImage.WritePixels(new Int32Rect(0, 0, _imageMD.XRes, _imageMD.YRes), _imageMD.ImageMapPtr, (int)_imageMD.DataSize, _cameraImage.BackBufferStride);
                }
                _cameraImage.Unlock();

                return _cameraImage;
            }
        }

        /// <summary>
        /// Camera depth image source.
        /// </summary>
        public ImageSource DepthImage
        {
            get
            {
                if (_cameraDepth != null)
                {
                    UpdateHistogram(_depthMD);

                    _cameraDepth.Lock();

                    unsafe
                    {
                        ushort* pDepth = (ushort*)_depthGenerator.GetDepthMapPtr().ToPointer();
                        // Pour avoir que les pixels de user 0
                        ushort* pLabels = (ushort*)_userGenerator.GetUserPixels(0).SceneMapPtr.ToPointer();
                        for (int y = 0; y < _depthMD.YRes; ++y)
                        {
                            byte* pDest = (byte*)_cameraDepth.BackBuffer.ToPointer() + y * _cameraDepth.BackBufferStride;
                            for (int x = 0; x < _depthMD.XRes; ++x, ++pDepth, ++pLabels, pDest += 3)
                            {
                                // Initialise le pixel a 0
                                pDest[0] = pDest[1] = pDest[2] = 0;
                                ushort label = *pLabels;
                                bool shouldDrawBackground = true;
                                // si on doit dessiner le fond ou si c'est le user.
                                if (shouldDrawBackground || *pLabels != 0)
                                {
                                    byte pixel = (byte)Histogram[*pDepth];
                                    pDest[0] = 0;
                                    pDest[1] = pixel;
                                    pDest[2] = pixel;
                                }
                            }
                        }
                    }

                    _cameraDepth.AddDirtyRect(new Int32Rect(0, 0, _depthMD.XRes, _depthMD.YRes));
                    _cameraDepth.Unlock();
                }

                return _cameraDepth;
            }
        }

        #endregion

        #region Event
        
        /// <summary>
        /// Camera event handler.
        /// </summary>
        /// <param name="sender">The object firing the event.</param>
        public delegate void CameraHandler(object sender);

        /// <summary>
        /// Camera event
        /// </summary>
        public event CameraHandler Camera;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new instance of OpenNI context 
        /// with the default xml configuration file.
        /// </summary>
        public KinectAPIContext() 
            : this(XML_CONFIGURATION)
        {
        }

        /// <summary>
        /// Creates a new instance of Open NI context 
        /// with the specified oni or xml configuration file.
        /// </summary>
        /// <param name="configuration"></param>
        public KinectAPIContext(string configuration)
        {
            if (configuration.EndsWith(".xml", System.StringComparison.CurrentCultureIgnoreCase))
            {
                InitialiseWithXML(configuration);
            }
            else if (configuration.EndsWith(".oni", System.StringComparison.CurrentCultureIgnoreCase))
            {
                InitialiseWithONI(configuration);
            }
            else
            {
                throw new Exception("File extension allowed are XML and ONI.");
            }
        }

        #endregion

        #region private methods

        /// <summary>
        /// Represents the single OpenNI context which will be created
        /// with oni configuration file
        /// </summary>
        /// <returns>OpenNI context</returns>
        public Context createContext()
        {
            if (_context == null)
                _context = new Context();

            return _context;
        }

        /// <summary>
        /// Represents the single OpenNI context which will be created
        /// with xml configuration file
        /// </summary>
        /// <param name="config">Xml configuration file</param>
        /// <returns>OpenNI context</returns>
        public Context createContext(string config)
        {
            if (_context == null)
                _context = new Context(config);

            return _context;
        }

        /// <summary>
        /// Initialize context with the oni configuration file
        /// </summary>
        /// <param name="configuration">Oni configuration file</param>
        private void InitialiseWithONI(string configuration)
        {
            try
            {
                _context = createContext();
                _context.OpenFileRecording(configuration);
                Initialise();
            }
            catch
            {
                throw new Exception("Configuration file not found.");
            }
        }

        /// <summary>
        /// Initialize with the xml configuration file
        /// </summary>
        /// <param name="configuration">Xml configuration file</param>
        private void InitialiseWithXML(string configuration)
        {
            try
            {
                _context = createContext(configuration);
                Initialise();
            }
            catch
            {
                throw new Exception("Configuration file not found.");
            }
        }

        /// <summary>
        /// Initialise the record and start the general thread
        /// </summary>
        private void Initialise()
        {
            _imageMD = new ImageMetaData();
            _depthMD = new DepthMetaData();
            _imageGenerator = _context.FindExistingNode(NodeType.Image) as ImageGenerator;
            _depthGenerator = _context.FindExistingNode(NodeType.Depth) as DepthGenerator;

            _userGenerator = new UserGenerator(_context);

            Histogram = new int[_depthGenerator.GetDeviceMaxDepth()];

            MapOutputMode mapMode = _depthGenerator.GetMapOutputMode();

            Width = (int)mapMode.nXRes;
            Height = (int)mapMode.nYRes;

            _cameraImage = new WriteableBitmap(Width, Height, DPI_X, DPI_Y, PixelFormats.Rgb24, null);
            _cameraDepth = new WriteableBitmap(Width, Height, DPI_X, DPI_Y, PixelFormats.Rgb24, null);

            _cameraThread = new Thread(CameraThread);
            _cameraThread.IsBackground = true;
            _cameraThread.Start();
        }

        /// <summary>
        /// Executed every time, while the thread is active
        /// Update the depth image
        /// </summary>
        private void CameraThread()
        {
            while (true)
            {
                try
                {
                    _context.WaitOneUpdateAll(_depthGenerator);
                }
                catch
                {
                    System.Diagnostics.Debug.WriteLine(string.Format("Error updating context on {0}, {1}", DateTime.Now.ToShortDateString(), DateTime.Now.ToShortTimeString()));
                }

                _imageGenerator.GetMetaData(_imageMD);
                _depthGenerator.GetMetaData(_depthMD);

                if (Camera != null)
                {
                    Camera(this);
                }
                if (recorder != null)
                {
                    recorder.Record();
                }

            }
        }

        /// <summary>
        /// Re-creates the depth histogram.
        /// </summary>
        /// <param name="depthMD"></param>
        public unsafe void UpdateHistogram(DepthMetaData depthMD)
        {
            // Reset.
            for (int i = 0; i < Histogram.Length; ++i)
                Histogram[i] = 0;

            ushort* pDepth = (ushort*)depthMD.DepthMapPtr.ToPointer();

            int points = 0;
            for (int y = 0; y < depthMD.YRes; ++y)
            {
                for (int x = 0; x < depthMD.XRes; ++x, ++pDepth)
                {
                    ushort depthVal = *pDepth;
                    if (depthVal != 0)
                    {
                        Histogram[depthVal]++;
                        points++;
                    }
                }
            }

            for (int i = 1; i < Histogram.Length; i++)
            {
                Histogram[i] += Histogram[i - 1];
            }

            if (points > 0)
            {
                for (int i = 1; i < Histogram.Length; i++)
                {
                    Histogram[i] = (int)(256 * (1.0f - (Histogram[i] / (float)points)));
                }
            }
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Start to recording the video.
        /// </summary>
        public void StartRecording()
        {
            StartRecording(RECORD_CONFIGURATION);
        }

        /// <summary>
        /// Strat the recording in the file at location filepath
        /// </summary>
        /// <param name="filePath">The file where the video is recorded (image and depth)</param>
        public void StartRecording(string filePath)
        {
            recorder = new Recorder(_context);
            recorder.SetDestination(RecordMedium.File, filePath);
            ProductionNode imageNode = _context.FindExistingNode(NodeType.Image);
            ProductionNode depthNode = _context.FindExistingNode(NodeType.Depth);
            recorder.AddNodeToRecording(imageNode);
            recorder.AddNodeToRecording(depthNode);
        }

        /// <summary>
        /// Stop the recording
        /// </summary>
        public void EndRecording()
        {
            ProductionNode imageNode = _context.FindExistingNode(NodeType.Image);
            ProductionNode depthNode = _context.FindExistingNode(NodeType.Depth);
            recorder.RemoveNodeFromRecording(imageNode);
            recorder.RemoveNodeFromRecording(depthNode);
            recorder = null;
        }

        /// <summary>
        /// Return if the context is recorded or not.
        /// </summary>
        /// <returns>True if the context is recorded, False if not.</returns>
        public bool isRecording()
        {
            return (recorder != null);
        }
        #endregion
    }
}
